// =============================================================================
//  Spangles Analyzer
//
//  Copyright(c) 2008
//  See LICENSE.txt for licensing information.
// =============================================================================

package com.google.spangles.imaging.impl;

import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;

import com.google.spangles.imaging.GeneratorService;
import com.google.spangles.view.renderers.GraphicsContext;

/**
 * (Description) (Any invariants [conditions that are always true] ) (Usage
 * instructions, and/or examples) (TODOs, ASSUMPTIONs, etc.) (Known Bugs)
 * (Concurrency strategy - not thread safe or how it's thread safe)
 * 
 * @author Chris
 */
public class GeneratorServiceImpl implements GeneratorService
{
   interface GraphicsOutputter
   {
      void outputGraphics(BufferedImage bi) throws FileNotFoundException,
                                           IOException;
   }

   class FileOutputter implements GraphicsOutputter
   {
      List<File> fileList = new LinkedList<File>();

      int index = 0;

      /*
       * (non-Javadoc)
       * 
       * @see com.google.spangles.imaging.impl.GeneratorServiceImpl.GraphicsOutputter#outputGraphics(java.awt.image.BufferedImage)
       */
      public void outputGraphics(BufferedImage bi)
                                                  throws FileNotFoundException,
                                                  IOException
      {
         File f = fileList.get(index++);

         ImageWriter iw = ImageIO.getImageWritersByFormatName("png").next();
         FileImageOutputStream fios = new FileImageOutputStream(f);
         iw.setOutput(fios);
         iw.write(bi);
         fios.close();
      }

      /**
       * (Description - what the method does and why) (Visibility decision, if
       * not obvious) (Pre/Post Conditions) (Usage examples) (Algorithmic notes)
       * (Concurrency issues)
       * 
       * @param file
       */
      public void addFile(File file)
      {
         fileList.add(file);
      }

   }

   /*
    * (non-Javadoc)
    * 
    * @see com.google.spangles.imaging.GeneratorService#generate()
    */
   public void generate() throws FileNotFoundException, IOException
   {
      int xsize = 20;
      int d = 4;
      int ysize = calculateYSize(xsize);

      int[] pen =
      {
               0x00EF, 0x00EF, 0x00EF, 0x00FF
      };
      int[] gray =
      {
               0x007F, 0x007F, 0x007F, 0x00FF
      };
      int contrast = 8;

      FileOutputter outputter = new FileOutputter();
      outputter.addFile(new File("test.png"));
      outputter.addFile(new File("test2.png"));

      generateTiles(xsize, ysize, d, pen, gray, contrast, outputter);
   }

   private void generateTiles(int xsize, int ysize, int d, int[] pen,
                              int[] gray, int contrast,
                              GraphicsOutputter outputter)
                                                          throws FileNotFoundException,
                                                          IOException
   {
      int dd = d * d;

      // ysize is the odd number nearest to sqrt(3).xsize

      // we must prove we can generate with transparency.
      BufferedImage bi = new BufferedImage(2 * xsize + 1, ysize + 1,
                                           BufferedImage.TYPE_4BYTE_ABGR);
      WritableRaster wr = bi.getRaster();

      BufferedImage bi2 = new BufferedImage(2 * xsize + 1, ysize + 1,
                                            BufferedImage.TYPE_4BYTE_ABGR);
      WritableRaster wr2 = bi2.getRaster();

      {
         int[] b = new int[4];// RGBA
         b[0] = 0;
         b[1] = 0;
         b[2] = 0;
         b[3] = 0;
         // fill the raster with transparent.
         for (int x = 0; x <= 2 * xsize; x++)
         {
            for (int y = 0; y <= ysize; y++)
            {
               wr.setPixel(x, y, b);
               wr2.setPixel(x, y, b);
            }
         }
      }

      // draw gray at the top
      for (int x = 0; x <= 2 * xsize; x++)
      {
         wr.setPixel(x, 0, gray);
         wr2.setPixel(x, ysize, gray);
      }

      int[] h2 = generateHighlight(pen, 2 * contrast);
      int[] h1 = generateHighlight(pen, contrast);
      int[] l1 = generateHighlight(pen, -contrast);
      int[] l2 = generateHighlight(pen, -2 * contrast);

      // draw the two diagonal lines. Note we need to move along xsize in ysize
      // rows
      // nearest integer to xsize*y/ysize
      int x = 0;
      int xfrac = ysize;
      for (int y = 1; y <= ysize; y++)
      {
         xfrac += (2 * xsize);
         if (xfrac >= (2 * ysize))
         {
            x++;
            xfrac -= (2 * ysize);
         }
         wr.setPixel(x, y, gray);
         wr.setPixel(2 * xsize - x, y, gray);
         wr2.setPixel(x, ysize - y, gray);
         wr2.setPixel(2 * xsize - x, ysize - y, gray);

         if (pen != null)
         {
            // fill in between with black
            for (int xx = x + 1; xx < 2 * xsize - x; xx++)
            {
               // calculate distances to the given lines
               int d1 = distance(xx, y, 0, 0, 2 * xsize, 0);
               int d2 = distance(xx, y, 0, 0, xsize, ysize);
               int d3 = distance(xx, y, xsize, ysize, 2 * xsize, 0);

               if ((d1 < dd) || (d2 < dd) || (d3 < dd))
               {
                  // if d1 is the smallest
                  if ((d1 < d2) && (d1 < d3))
                  {
                     wr.setPixel(xx, y, h2);
                     wr2.setPixel(xx, ysize - y, l2);
                  }
                  else if (d3 < d2)
                  {
                     wr.setPixel(xx, y, h1);
                     wr2.setPixel(xx, ysize - y, h1);
                  }
                  else
                  {
                     wr.setPixel(xx, y, l1);
                     wr2.setPixel(xx, ysize - y, l1);
                  }
               }
               else
               {
                  wr.setPixel(xx, y, pen);
                  wr2.setPixel(xx, ysize - y, pen);
               }
            }
         }
      }

      outputter.outputGraphics(bi);
      outputter.outputGraphics(bi2);
   }

   /**
    * (Description - what the method does and why) (Visibility decision, if not
    * obvious) (Pre/Post Conditions) (Usage examples) (Algorithmic notes)
    * (Concurrency issues)
    * 
    * @param pen
    * @param i
    * @return
    */
   private int[] generateHighlight(int[] pen, int i)
   {
      if (pen == null)
      {
         return null;
      }

      int[] hl = new int[4];
      hl[0] = pen[0] + i;
      hl[1] = pen[1] + i;
      hl[2] = pen[2] + i;
      hl[3] = 0x00FF;
      return hl;
   }

   /**
    * (Description - what the method does and why) (Visibility decision, if not
    * obvious) (Pre/Post Conditions) (Usage examples) (Algorithmic notes)
    * (Concurrency issues)
    * 
    * @param xsize
    * @return
    */
   private int calculateYSize(int xsize)
   {
      return oddRoot(3 * xsize * xsize);
   }

   /**
    * (Description - what the method does and why) (Visibility decision, if not
    * obvious) (Pre/Post Conditions) (Usage examples) (Algorithmic notes)
    * (Concurrency issues)
    * 
    * @param xx
    * @param y
    * @param xsize
    * @param ysize
    * @param i
    * @param j
    * @return
    */
   private int distance(int x, int y, int x0, int y0, int x1, int y1)
   {
      int lx = x1 - x0;
      int ly = y1 - y0; // line vector, perp is (ly,-lx);
      int dx = x - x0;
      int dy = y - y0;

      int p = dx * ly - dy * lx;

      // hang on, p needs to be normalized.
      // just return the square
      return ((p * p) / (lx * lx + ly * ly));
   }

   /**
    * (Description - what the method does and why) (Visibility decision, if not
    * obvious) (Pre/Post Conditions) (Usage examples) (Algorithmic notes)
    * (Concurrency issues)
    * 
    * @param i
    * @return
    */
   private int oddRoot(int x)
   {
      // this is a cheap integer square root. Ish.
      int r2 = 1;
      while (r2 * r2 < x)
      {
         r2 *= 2;
      }
      int r1 = x / r2;

      while (r2 - r1 > 1)
      {
         int s = (r1 + r2) / 2;
         if (s * s < x)
         {
            r1 = s;
            r2 = x / s;
         }
         else
         {
            r2 = s;
            r1 = x / s;
         }
      }

      if ((r1 & 1) != 0)
      {
         return r1;
      }
      return r2;
   }

   class ContextOutputter implements GraphicsOutputter
   {
      GraphicsContext gc;

      /**
       * @param gc
       */
      public ContextOutputter(GraphicsContext gc)
      {
         this.gc = gc;
      }

      /*
       * (non-Javadoc)
       * 
       * @see com.google.spangles.imaging.impl.GeneratorServiceImpl.GraphicsOutputter#outputGraphics(java.awt.image.BufferedImage)
       */
      public void outputGraphics(BufferedImage bi)
                                                  throws FileNotFoundException,
                                                  IOException
      {
         gc.addImage(bi);
      }

   }

   /*
    * (non-Javadoc)
    * 
    * @see com.google.spangles.imaging.GeneratorService#generateImages(com.google.spangles.view.renderers.GraphicsContext)
    */
   public void generateImages(GraphicsContext gc) throws FileNotFoundException,
                                                 IOException
   {
      int xsize = gc.getXSize();
      int ysize = calculateYSize(xsize);
      gc.setYSize(ysize);
      int bevel = gc.getBevel();

      int contrast = gc.getContrast();

      GraphicsOutputter outputter = new ContextOutputter(gc);

      generateTiles(xsize, ysize, bevel, gc.getWhitePen(), gc.getGrayPen(),
                    contrast, outputter);
      generateTiles(xsize, ysize, bevel, gc.getBlackPen(), gc.getGrayPen(),
                    contrast, outputter);
      generateTiles(xsize, ysize, bevel, gc.getGrayPen(), gc.getGrayPen(),
                    contrast, outputter);
      generateTiles(xsize, ysize, bevel, null, gc.getGrayPen(), contrast,
                    outputter);

   }

}
